<?php
/**
 * Created by PhpStorm.
 * User: inhere
 * Date: 2016/9/27
 * Time: 14:17
 */

namespace app\components;

defined('IN_CONSOLE') || define('IN_CONSOLE', false);

/**
 * base application class
 * Class BaseApp
 * @package app\components
 *
 * @Usage please extends the class. e.g:
 *
 * ```
 * class App extends BaseApp
 * {
 * }
 * $app = new App(...);
 * $app->run();
 * ```
 *
 * @property Input input
 * @property Output output
 * @property SimpleRenderer view
 *
 * @method static Input input()
 * @method static Output output()
 * @method static SimpleRenderer view()
 *
 */
abstract class BaseApp
{
    /**
     * @var static
     */
    public static $me;

    /**
     * app default config file name
     * @var string
     */
    protected $defaultConfigFile = 'config.ini';

    /**
     * there are required services before run.
     * @var array
     */
    protected $requiredServices = ['logger', 'input', 'output'];

    /**
     * app config
     * @var array
     */
    protected $config = [
        'env' => 'pdt', // dev test cli
        'debug' => false,
        'charset' => 'UTF-8',
        'timeZone' => 'Asia/Shanghai',
        // 'defaultRoute' => '',
    ];

    /**
     * app local config
     * @var array
     */
    private static $_local = [
        '__loaded' => false,
        'started' => false,
    ];

    /**
     * path alias
     * @var array
     */
    private static $aliases = [];

    /**
     * all raw register service list
     * @var array
     */
    private static $services = [];

    /**
     * all parsed instance list by call service.
     * @var array
     */
    private static $instances = [];

    const DIR_SEP = '/';
    const LOC_CONF_FILE = '.env';

    // env name list
    const ENV_PDT  = 'pdt';
    const ENV_CLI  = 'cli';
    const ENV_DEV  = 'dev';
    const ENV_TEST = 'test';

    // event name list
    const EVT_BEFORE_RUN = 'onBeforeRun';
    const EVT_AFTER_RUN  = 'onAfterRun';
    const EVT_APP_STOP   = 'onAppStop';
    const EVT_NOT_FOUND  = 'onNotFound';

    /**********************************************************
     * init app
     **********************************************************/

    /**
     * BaseApp constructor.
     * @param string $config
     */
    public function __construct($config = '')
    {
        self::$me = $this;

        // fixed: for support php 5.5
        self::$aliases = array_merge([
            '@project' => PROJECT_PATH,
            '@app'     => PROJECT_PATH . '/app',
            '@config'  => PROJECT_PATH . '/config',
            '@web'     => PROJECT_PATH . '/web',
            '@assets'  => PROJECT_PATH . '/web/assets',
            '@temp'    => PROJECT_PATH . '/temp',
            '@vendor'  => PROJECT_PATH . '/vendor',
        ],self::$aliases);

        $this->init($config);
    }

    public function init($config)
    {
        $config = $config ?: PROJECT_PATH . '/config/config.' .APP_ENV. '.ini';
        $this->loadConfig($config);

        defined('APP_DEBUG') || define('APP_DEBUG', $this->config('debug', false));

        $this->config['env']   = APP_ENV;
        $this->config['debug'] = APP_DEBUG;
        $this->config['started'] = true;
        $this->config['startTime'] = $_SERVER['REQUEST_TIME_FLOAT'];

        date_default_timezone_set($this->config('timeZone', 'UTC'));
    }

    /**********************************************************
     * app run
     **********************************************************/

    protected function prepareRun()
    {
        // Check whether the necessary services has been registered
        $pub = array_intersect($this->requiredServices, self::getServiceNames());

        if ( count($pub) !== count($this->requiredServices) ) {
            throw new \RuntimeException('Please register required service. required:' . implode(',', $this->requiredServices));
        }
    }

    /**
     * run app
     * @param bool $exit
     */
    public function run($exit=true)
    {
        $this->prepareRun();

        // call 'onBeforeRun' service, if it is registered.
        self::factory(self::EVT_BEFORE_RUN);

        // do run ...
        $this->doRun();

        // call 'onAfterRun' service, if it is registered.
        self::factory(self::EVT_AFTER_RUN);

        if ($exit) {
            $this->stop();
        }
    }

    /**
     * do run app, dispatch route.
     */
    protected function doRun()
    {
        throw new \LogicException('Please override it on the sub class.');
    }

    public function stop()
    {
        // call 'onAppStop' service, if it is registered.
        self::factory(self::EVT_APP_STOP);

        self::$_local['endTime'] = microtime(true);

        exit(0);
    }

    /**********************************************************
     * app config
     *   config priority: .env > config.{ENV}.ini > config.ini > property $config
     *   '.env' and 'config.ini' is optional, when they exist will be loaded.
     **********************************************************/

    /**
     * load app config file
     * @param string $file config file
     * @return void
     */
    protected function loadConfig($file)
    {
        if ( !is_file($file) ) {
            throw new \RuntimeException("Config file don't exists. FILE: $file");
        }

        $config = $this->config;

        // default config
        $dFile = static::alias('@config/' . $this->defaultConfigFile);

        if ( is_file($dFile) ) {
            $defaultConfig = parse_ini_file($dFile, true);
            unset($defaultConfig['env']);

            $config = PublicHelper::arrayMerge($config, $defaultConfig);
        }

        // current env config
        $envConfig = parse_ini_file($file, true);
        unset($envConfig['env']);

        $config = PublicHelper::arrayMerge($config, $envConfig);

        // local env config
        $locConfig = self::env();
        unset($locConfig['__loaded'], $locConfig['started']);

        $this->config = PublicHelper::arrayMerge($config, $locConfig);

        // fixed 'env' in console.
        if ( IN_CONSOLE ) {
            $this->config['env'] = self::$_local['env'] = 'cli';
        }
    }

    /**
     * get/set config
     * @param  string|array $name
     * @param  mixed $default
     * @return mixed
     */
    public function config($name, $default=null)
    {
        // `$name` is array, set config.
        if (is_array($name)) {
            foreach ($name as $key => $value) {
                $this->config[$key] = $value;
            }

            return true;
        }

        // is string, get config
        if (!is_string($name)) {
            return $default;
        }

        // allow get $config['top']['sub'] by 'top.sub'
        if ( strpos($name, '.') > 1 ) {
            list($topKey, $subKey) = explode('.', $name, 2);

            if ( isset($this->config[$topKey]) && isset($this->config[$topKey][$subKey])) {
                return $this->config[$topKey][$subKey];
            }
        }

        return isset($this->config[$name]) ? $this->config[$name]: $default;
    }

    /**
     * get config
     * @return array
     */
    public function getConfig()
    {
        return $this->config;
    }

    /**
     * is Debug
     * @return boolean
     */
    public function isDebug()
    {
        return (bool)$this->config('debug', false);
    }

    /**********************************************************
     * app local env config
     **********************************************************/

    /**
     * get local env config
     * @param  string $name
     * @param  mixed $default
     * @return mixed
     */
    public static function env($name = null, $default=null)
    {
        // init loading ...
        if ( self::$_local['__loaded'] === false ) {
            $file = PROJECT_PATH .'/'. self::LOC_CONF_FILE;
            self::$_local['__loaded'] = true;

            if ( is_file($file) ) {
                self::$_local = array_merge(self::$_local, parse_ini_file($file, true));
            }
        }

        // get all
        if ( null === $name ) {
            return self::$_local;
        }

        // get one by key name
        return isset(self::$_local[$name]) ? self::$_local[$name] : $default;
    }


    /**********************************************************
     * app alias
     **********************************************************/

    /**
     * set/get path alias
     * @param array|string $path
     * @param string|null $value
     * @return bool|string
     */
    public static function alias($path, $value=null)
    {
        // get path by alias
        if ( is_string($path) && !$value ) {
            // don't use alias
            if ( $path[0] !== '@' ) {
                return $path;
            }

            $path = str_replace(['/','\\'], self::DIR_SEP , $path);

            // only a alias. e.g. @project
            if ( !strpos($path, self::DIR_SEP) ) {
                return isset(self::$aliases[$path]) ? self::$aliases[$path] : $path;
            }

            // have other partial. e.g: @project/temp/logs
            $realPath = $path;
            list($alias, $other) = explode(self::DIR_SEP, $path, 2);

            if ( isset(self::$aliases[$alias]) ) {
                $realPath = self::$aliases[$alias] . self::DIR_SEP . $other;
            }

            return $realPath;
        }

        if ( $path && $value && is_string($path) && is_string($value) ) {
            $path = [$path => $value];
        }

        // custom set path's alias. e.g: Slim::alias([ 'alias' => 'path' ]);
        if ( is_array($path) ) {
            foreach ($path as $alias => $realPath) {
                // 1th char must is '@'
                if ( $alias[0] !== '@' ) {
                    continue;
                }

                self::$aliases[$alias] = $realPath;
            }
        }

        return true;
    }

    /**
     * @return array
     */
    public static function getAliases()
    {
        return self::$aliases;
    }

    /**********************************************************
     * app service
     **********************************************************/

    /**
     * register a app service
     * @param string $name the service name
     * @param object|\Closure $service service
     * @param bool $replace replace exists service
     * @return static
     */
    public function register($name, $service, $replace = false)
    {
//        static::get('logger')->debug('Init app component: ' . $name);

        self::set($name, $service, $replace);

        return $this;
    }

    /**
     * register a app service
     * @param string $name
     * @param object $service
     * @param bool $replace replace exists service
     * @return bool
     */
    public static function set($name, $service, $replace = false)
    {
        if ( !isset(self::$services[$name]) || $replace ) {
            self::$services[$name] = $service;
        }

        return true;
    }

    /**
     * get a app service by name
     * if is a closure, only run once.
     * @param string $name
     * @param bool $call if service is 'Closure', call it.
     * @return mixed
     */
    public static function get($name, $call = true)
    {
        if ( !isset(self::$services[$name]) ) {
            return null;
        }

        $service = self::$services[$name];

        if ( is_object($service) && $service instanceof \Closure && $call) {
            if ( !isset(self::$instances[$name]) ) {
                self::$instances[$name] = $service(self::$me);
            }

            return self::$instances[$name];
        }

        return $service;
    }

    /**
     * create a app service by name
     * it always return a new instance.
     * @param string $name
     * @return object
     */
    public static function factory($name)
    {
        if ( !isset(self::$services[$name]) ) {
            return null;
        }

        $service = self::$services[$name];

        if ( is_object($service) && $service instanceof \Closure ) {
            return $service(self::$me);
        }

        return $service;
    }

    /**
     * @param $name
     * @return bool
     */
    public static function has($name)
    {
        return isset(self::$services[$name]);
    }

    /**
     * @return array
     */
    public static function getServiceNames()
    {
        return array_keys(self::$services);
    }

    /**
     * allow register a app service by property
     * ```
     * $app->logger = function(){
     *     return new xx\yy\Logger;
     * };
     * ```
     * @param string $name
     * @param object $service
     * @return bool
     */
    public function __set($name, $service)
    {
        return self::set($name, $service);
    }

    /**
     * allow call service by property
     * ```
     * $logger = App::$me->logger;
     * ```
     * @param  string $name service name
     * @return object
     */
    public function __get($name)
    {
        return self::get($name);
    }

    /**
     * allow call service by static method
     * @param  string $name service name
     * @param  array $args
     * @return object
     */
    public static function __callStatic($name, $args)
    {
        return self::get($name);
    }
}
